POST /promoter-association/curator/affiliate-link

[!summary] 接口概述 由帖子所有者(Curator)为其他推广者创建联盟推广链接,使其能够转发销售该帖子的产品。此接口会自动创建或查找推广者账户,并返回用于生成推广链接的关键信息。


接口功能说明

此接口用于 帖子所有者(Curator/Post Owner) 为其他用户创建联盟推广链接。主要功能包括:

  1. 自动用户管理:根据手机号或邮箱查找已验证用户,若不存在则自动创建联盟推广者账户
  2. 权限验证:仅帖子所有者可为其帖子创建推广链接
  3. 业务规则验证
    • 防止为自己的帖子创建推广链接
    • 防止已在销售链中的用户重复参与
    • 防止产品所有者参与推广
  4. 推广链接生成:返回 affiliateCode 用于构造推广链接

请求地址 & 请求方式

POST /promoter-association/curator/affiliate-link

环境地址

  • 生产环境:https://release.katana-api.1m.app
  • 开发环境:根据实际配置

请求头(Header)

Header 名称 值示例 必填 说明
Authorization Bearer eyJhbGci... JWT 认证令牌
Content-Type application/json 请求内容类型
from client - 请求来源标识
x-track-id UUID - 请求追踪 ID
timezone Asia/Shanghai - 时区信息

请求参数

Body 参数(JSON)

字段名 类型 必填 说明 验证规则
curatorId string (UUID) 帖子所有者的用户 ID 必须是当前登录用户的 ID
postAlias string 帖子的 URL 别名 帖子必须允许转发销售 (allowPromotersResell: true)
phoneNumberOrEmail string 推广者的手机号或邮箱 必须是有效的手机号或邮箱格式

请求示例

{
  "curatorId": "1ee2b015-5390-44ba-a677-8cbd53e8066f",
  "postAlias": "000011",
  "phoneNumberOrEmail": "neo.wang.ext+21@1m.app"
}

响应结构 & 字段说明

响应结构

{
  "code": number,        // HTTP 状态码
  "message": string,     // 响应消息
  "request_id": string,  // 请求追踪 ID
  "data": {
    "curator": {
      "id": string,           // 帖子所有者用户 ID
      "vanityUrl": string     // 帖子所有者的个性 URL
    },
    "post": {
      "id": string,           // 帖子 ID
      "alias": string         // 帖子别名
    },
    "promoter": {
      "id": string,           // 推广者用户 ID
      "email": string,        // 推广者邮箱(可能为 null)
      "phoneNumber": string,  // 推广者手机号(可能为 null)
      "vanityUrl": string     // 推广者个性 URL(可能为 null)
    },
    "affiliateCode": string   // 推广者的联盟推广代码
  }
}

字段说明

路径 字段 类型 说明
data.curator.id 帖子所有者 ID string (UUID) 创建推广链接的用户 ID
data.curator.vanityUrl 帖子所有者个性 URL string 用于构建帖子所有者的店铺链接
data.post.id 帖子 ID string (UUID) 被推广的帖子 ID
data.post.alias 帖子别名 string 帖子的 URL 别名
data.promoter.id 推广者 ID string (UUID) 推广者的用户 ID
data.promoter.email 推广者邮箱 string \ null 推广者的注册邮箱
data.promoter.phoneNumber 推广者手机号 string \ null 推广者的注册手机号
data.promoter.vanityUrl 推广者个性 URL string \ null 推广者的店铺链接前缀
data.affiliateCode 推广代码 string 用于生成推广链接的唯一代码

成功示例

HTTP 200 成功响应

{
  "code": 200,
  "message": "success",
  "request_id": "783af55d-5ff4-4874-8ff3-0e0482e09725",
  "data": {
    "curator": {
      "id": "1ee2b015-5390-44ba-a677-8cbd53e8066f",
      "vanityUrl": "neo"
    },
    "post": {
      "id": "1316f4a1-f5c7-465d-bb59-aaef9bbcbd47",
      "alias": "000011"
    },
    "promoter": {
      "id": "310ed6c9-dbf9-4b44-817d-54e8a940d4e8",
      "email": "neo.wang.ext+21@1m.app",
      "phoneNumber": null,
      "vanityUrl": null
    },
    "affiliateCode": "ehf843"
  }
}

错误示例

401 Unauthorized - 未认证

{
  "statusCode": 401,
  "message": "Unauthorized"
}

场景:请求头中缺少有效的 Authorization 令牌。


400 Bad Request - 无权限操作

{
  "statusCode": 400,
  "message": "You do not have the permission to perform this operation.",
  "error": "Bad Request"
}

场景curatorId 与当前登录用户 ID 不匹配,即用户无权为他人创建推广链接。


400 Bad Request - 手机号或邮箱格式无效

{
  "statusCode": 400,
  "message": "PhoneNumber or email is invalid.",
  "error": "Bad Request"
}

场景phoneNumberOrEmail 参数既不是有效的手机号格式,也不是有效的邮箱格式。


400 Bad Request - 不能为自己创建推广链接

{
  "statusCode": 400,
  "message": "Can not create affiliate-link for the post owner.",
  "error": "Bad Request"
}

场景:推广者(根据手机号/邮箱查找的用户)就是帖子所有者本人。


400 Bad Request - 推广者已在销售链中

{
  "statusCode": 400,
  "message": "The promoter has already resold this post",
  "error": "Bad Request"
}

场景:推广者已经在这个帖子的销售链中(其 ID 存在于 post.uplineCreatorList)。

[!info] 业务逻辑 为防止用户在单个销售链中重复出现,系统会检查推广者是否已参与该帖子的转售。这可以避免佣金计算混乱和循环引用问题。


400 Bad Request - 不能为产品所有者创建推广链接

{
  "statusCode": 400,
  "message": "Can not create affiliate-link for the product owner",
  "error": "Bad Request"
}

场景:推广者是帖子中关联产品的所有者。

[!info] 业务逻辑 产品所有者不能作为自己产品的推广者,以避免业务逻辑冲突。


404 Not Found - 帖子所有者或帖子不存在

{
  "statusCode": 404,
  "message": "Resource not found",
  "error": "Not Found"
}

场景

  1. curatorId 对应的用户不存在
  2. postAlias 对应的帖子不存在
  3. 帖子存在但 allowPromotersResellfalse

注意事项 & 业务逻辑

认证与授权

[!warning] 认证要求 此接口 必须认证,请求头中必须包含有效的 Authorization: Bearer <token>

[!warning] 权限验证 只有帖子的 所有者 才能为其帖子创建推广链接。系统会验证 curatorId 必须与当前登录用户 ID 一致。


用户创建逻辑

[!important] 自动创建推广者账户 当根据 phoneNumberOrEmail 查找不到用户时,系统会自动创建一个新的联盟推广者账户:

  • 用户角色PROMOTER
  • 用户类型AFFILIATE
  • 邮箱:若提供邮箱,则设置为该邮箱
  • 手机号:若提供手机号,则设置为该手机号

[!note] 查找用户优先级 系统使用 findAffiliateOrVerifiedUserByPhoneNumberOrEmail 方法查找用户,优先返回:

  1. 联盟类型(AFFILIATE)用户
  2. 已验证手机号或邮箱的标准用户

帖子验证规则

[!warning] 帖子必须允许转发 系统会验证帖子的 allowPromotersResell 字段必须为 true,否则返回 404 错误。


销售链防重逻辑

[!important] 防止销售链重复 系统会检查以下情况以防止用户在销售链中重复出现:

  1. 帖子转售检查:检查推广者 ID 是否在 post.uplineCreatorList
  2. 产品所有者检查:检查推广者 ID 是否为 post.relatedProducts 中任何产品的所有者

违反任一规则将返回 400 错误。


推广链接构造

[!info] 推广链接格式 虽然此接口仅返回 affiliateCode,但完整的推广链接通常格式为:

https://<subdomain>/p/<curator_vanity_url>/<post_alias>?ref=<affiliate_code>

例如:https://release.pear.us/p/neo/000011?ref=ehf843


相关接口

接口 说明
POST /promoter-association/guest/affiliate-link 公开接口,访客也可创建推广链接
GET /promoter-association/affiliate-promoters 获取已创建的推广者列表
POST /promoter-association/affiliate-link/notification 发送推广链接创建通知(邮件/短信)

使用场景

  1. Curator 主动邀请:帖子所有者通过输入手机号/邮箱为特定用户创建推广权限
  2. 批量推广:Curator 为多个推广者批量创建推广链接
  3. 新用户转化:非注册用户通过此接口自动注册为联盟推广者

时区处理

[!note] 时区支持 请求头中的 timezone 参数会影响某些时间相关的计算,但此接口主要返回用户信息,时间戳使用 UTC 标准。


数据流与泳道图

系统架构概览

┌─────────────────────────────────────────────────────────────────────────────┐
│                              系统分层架构                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│  Client Layer          │  Controller Layer  │  Service Layer  │  Database │
│  (Frontend/Mobile)     │  (NestJS)         │  (Business)     │  (Prisma) │
└─────────────────────────────────────────────────────────────────────────────┘

完整数据流泳道图

sequenceDiagram
    autonumber
    participant Client as 客户端<br/>(Frontend)
    participant Controller as Controller<br/>PromoterAssociationController
    participant Service as Service<br/>PromoterAssociationService
    participant UserRepo as UserMetaRepository
    participant PARepo as PromoterAssociationRepository
    participant DB as Database<br/>(PostgreSQL)

    Note over Client,DB: === 阶段1: 请求验证与参数解析 ===
    Client->>Controller: POST /promoter-association/curator/affiliate-link<br/>{curatorId, postAlias, phoneNumberOrEmail}
    Controller->>Controller: 从 JWT Token 获取 userId
    Controller->>Controller: 验证 curatorId === userId

    alt 权限验证失败
        Controller-->>Client: 400 Bad Request<br/>"You do not have the permission..."
    end

    Controller->>Controller: 验证 phoneNumberOrEmail 格式<br/>(isEmail() || isPhoneNumber())

    alt 格式验证失败
        Controller-->>Client: 400 Bad Request<br/>"PhoneNumber or email is invalid."
    end

    Note over Client,DB: === 阶段2: 并行查询数据 ===
    par 并行查询三个数据源
        Controller->>UserRepo: findUserEntityById(curatorId)
        UserRepo->>DB: SELECT * FROM "User"<br/>WHERE id = curatorId
        DB-->>UserRepo: curator (UserEntity)
        UserRepo-->>Controller: curator

    and
        Controller->>PARepo: findPostByAliasAndCreatorId()<br/>{urlAlias, creatorId, allowPromotersResell}
        PARepo->>DB: SELECT * FROM "Post"<br/>WHERE urlAlias = postAlias<br/>AND creatorId = curatorId<br/>AND allowPromotersResell = true
        DB-->>PARepo: post (Post)
        PARepo-->>Controller: post

    and
        Controller->>UserRepo: findAffiliateOrVerifiedUserByPhoneNumberOrEmail()
        UserRepo->>DB: SELECT * FROM "User"<br/>WHERE (email = input OR phoneNumber = input)<br/>AND (type = 'AFFILIATE' OR emailVerified = true<br/>OR phoneNumberVerified = true)<br/>ORDER BY createdAt DESC
        DB-->>UserRepo: promoter (UserEntity | null)
        UserRepo-->>Controller: promoter
    end

    Note over Client,DB: === 阶段3: 业务规则验证 ===
    Controller->>Controller: 检查 curator && post 是否存在

    alt curator 或 post 不存在
        Controller-->>Client: 404 Not Found<br/>"Resource not found"
    end

    Controller->>Controller: 检查 promoter.userId !== curator.userId

    alt promoter 就是 curator
        Controller-->>Client: 400 Bad Request<br/>"Can not create affiliate-link<br/>for the post owner."
    end

    Controller->>Controller: 检查 promoter.userId 不在<br/>post.uplineCreatorList 中

    alt promoter 已在销售链中
        Controller-->>Client: 400 Bad Request<br/>"The promoter has already<br/>resold this post"
    end

    Controller->>Controller: 检查 promoter.userId 不是<br/>post.relatedProducts 的所有者

    alt promoter 是产品所有者
        Controller-->>Client: 400 Bad Request<br/>"Can not create affiliate-link<br/>for the product owner"
    end

    Note over Client,DB: === 阶段4: 用户创建或复用 ===
    alt promoter 不存在
        Controller->>Service: createGuestUser()<br/>{promoterEmail, promoterPhoneNumber}
        Service->>DB: INSERT INTO "User"<br/>(id, email, phoneNumber,<br/>userRole=CONSUMER, type=GUEST,<br/>createdFeature=AFFILIATE)
        DB-->>Service: newUser
        Service-->>Controller: promoterUserEntity
    else promoter 已存在
        Note over Controller: 复用现有 promoter
    end

    Note over Client,DB: === 阶段5: 构造响应 ===
    Controller->>Controller: toCreateAffiliateLinkResponse()<br/>{curator, promoter, post}
    Controller-->>Client: 200 OK<br/>{curator, post, promoter, affiliateCode}

涉及的数据表

表名 操作类型 说明
User SELECT 查询 Curator 信息
User SELECT 查询 Promoter 信息(根据 phone/email)
User INSERT (可选) 若 Promoter 不存在,创建新用户
Post SELECT 查询帖子信息并验证 allowPromotersResell
PromoterAssociation INSERT/UPDATE (新增) 创建或更新推广关联记录

[!info] ⚠️ PromoterAssociation 表的使用 此接口现在会创建 PromoterAssociation 记录(2026-02-26 更新)

记录内容

  • invitationType = 'AFFILIATE' - 标识为联盟推广类型
  • note = '{"postId": "xxx", "createdAt": "..."}' - 存储帖子元数据
  • allowSyncPosts = false - 联盟推广者默认不同步帖子

目的

  • 跟踪哪些推广者收到了哪个帖子的联盟链接
  • 支持帖子所有者查看已发送的推广链接
  • 区分"已发送链接"和"已建立合作关系"

[!tip] 数据库查询优化

  • 并行查询:阶段 2 中的三个查询(Curator、Post、Promoter)并行执行,减少响应时间
  • 索引使用:查询使用 idurlAlias + creatorId 等索引字段

关键业务节点说明

阶段 关键逻辑 代码位置
权限验证 userId !== curatorId 抛出异常 Controller:296-301
格式验证 isEmail()isPhoneNumber() Controller:303-306
帖子查询 allowPromotersResell: true 必须满足 Repo:326-329
用户查询优先级 AFFILIATE 类型 > 已验证用户 Repo:246-248
销售链防重 检查 uplineCreatorListrelatedProducts Controller:333-346
用户创建 userRole=CONSUMER, type=GUEST Service:313-323

数据流转图

graph LR
    subgraph Request["请求输入"]
        A[CuratorID]
        B[PostAlias]
        C[Phone/Email]
    end

    subgraph Validation["验证层"]
        D{JWT认证}
        E{格式验证}
        F{权限验证}
    end

    subgraph Query["查询层"]
        G[(User表<br/>Curator)]
        H[(Post表)]
        I[(User表<br/>Promoter)]
    end

    subgraph BusinessRules["业务规则"]
        J{不是自己?}
        K{不在销售链?}
        L{不是产品主?}
    end

    subgraph UserCreation["用户处理"]
        M{Promoter<br/>存在?}
        N[创建新用户<br/>GUEST/AFFILIATE]
        O[复用现有用户]
    end

    subgraph Response["响应输出"]
        P[Curator信息]
        Q[Post信息]
        R[Promoter信息]
        S[AffiliateCode]
    end

    A --> D
    B --> D
    C --> E
    D --> F
    E --> F
    F --> G
    F --> H
    F --> I

    G --> J
    H --> K
    I --> L

    J --> M
    K --> M
    L --> M

    M -->|否| N
    M -->|是| O
    N --> P
    O --> P

    G --> P
    H --> Q
    I --> R
    R --> S

    style Request fill:#e1f5fe
    style Validation fill:#fff3e0
    style Query fill:#f3e5f5
    style BusinessRules fill:#ffebee
    style UserCreation fill:#e8f5e9
    style Response fill:#e0f2f1

PromoterAssociation 表的生命周期

[!important] ⚠️ 重要更新 自 2026-02-26 起,createAffiliateLink 接口现在会创建 PromoterAssociation 记录

  • 此记录的 invitationType = AFFILIATE
  • note 字段存储 JSON 元数据:{"postId": "xxx", "createdAt": "2026-02-26T..."}
  • 这使得帖子所有者可以查看他们为每个帖子创建的所有联盟链接

以下是 PromoterAssociation 记录的完整生命周期:

graph LR
    subgraph Phase1["阶段1: 创建推广链接 (本接口)"]
        A[POST /curator/affiliate-link]
        B["验证: User + Post"]
        C["确保 Promoter 存在"]
        D["创建/更新 PromoterAssociation"]
        B --> C
        C --> D
        D --> E["✅ invitationType=AFFILIATE<br/>note={postId, createdAt}"]
    end

    subgraph Phase2["阶段2: 查询推广链接 (新接口)"]
        F["GET /curator/affiliate-links"]
        G["按 curatorId + postId 过滤"]
        F --> G
        G --> H["返回所有推广者列表"]
    end

    subgraph Phase3["阶段3: 接受邀请 (其他接口)"]
        I["用户注册/下单"]
        J["POST /accept-invitation"]
        K["更新为 INVITATION_LINK"]
        I --> J
        J --> K
    end

    Phase1 --> Phase2
    Phase2 --> Phase3

    style Phase1 fill:#e3f2fd
    style Phase2 fill:#fff3e0
    style Phase3 fill:#e8f5e9
    style E fill:#ffcdd2
    style H fill:#ffcdd2
    style K fill:#c8e6c9
阶段 接口/操作 涉及表 PromoterAssociation 状态
1. 准备链接 POST /curator/affiliate-link User, Post ❌ 不创建
2. 发送链接 POST /affiliate-link/notification 无变更 ❌ 不创建
3. 用户访问 前端解析 ?ref=xxx 无变更 ❌ 不创建
4. 用户注册 用户通过链接注册 User (创建) ❌ 不创建
5. 接受邀请 POST /accept-invitation PromoterAssociation 在此创建
6. 首次下单 下单时自动关联 PromoterAssociation 可能在此创建

[!tip] 设计理由 为什么不在本接口创建 PromoterAssociation?

  1. 避免垃圾数据:用户可能从未点击链接或注册
  2. 状态区分:区分"已发送邀请"和"已建立合作"
  3. 业务语义:推广链接只是"邀请函",真正接受邀请才算"关联"
  4. 性能优化:减少无效关联记录的存储和查询

相关代码文件

文件路径 说明
src/promoter-association/promoter-association.controller.ts 控制器,定义接口路由和请求处理逻辑 (行 290-368)
src/promoter-association/promoter-association.interface.ts 接口定义,包含 DTO 和响应类型
src/promoter-association/promoter-association.service.ts 服务层,实现 createGuestUser() 等业务逻辑
src/promoter-association/promoter-association.repo.ts 数据访问层,实现 findPostByAliasAndCreatorId()
src/user-meta/userMeta.repository.ts 用户元数据仓库,实现 findAffiliateOrVerifiedUserByPhoneNumberOrEmail()

GET /promoter-association/curator/affiliate-links

[!summary] 新增接口 - 查询帖子的联盟推广链接 帖子所有者可以查看他们为特定帖子创建的所有联盟推广链接记录。

新增日期: 2026-02-26


接口功能说明

此接口用于 帖子所有者(Curator) 查询他们为特定帖子创建的所有联盟推广链接。主要功能包括:

  1. 按帖子过滤:可查询特定帖子的所有联盟链接,或查询所有帖子的链接
  2. 推广者信息:返回每个推广者的详细信息(姓名、邮箱、手机号、个性 URL、Logo)
  3. 分页支持:支持分页查询
  4. 搜索功能:可根据推广者姓名、邮箱、手机号搜索
  5. 帖子信息:返回帖子标题、封面图等基本信息

请求地址 & 请求方式

GET /promoter-association/curator/affiliate-links

请求头(Header)

Header 名称 值示例 必填 说明
Authorization Bearer eyJhbGci... JWT 认证令牌

请求参数(Query)

字段名 类型 必填 说明
postAlias string 帖子别名,用于过滤特定帖子的链接(不传则返回所有帖子的链接)

请求示例

GET /promoter-association/curator/affiliate-links?postAlias=000011

GET /promoter-association/curator/affiliate-links

[!note] 无分页参数 此接口不支持分页,返回所有符合条件的记录。


响应结构 & 字段说明

响应结构

[
  {
    "id": string,              // PromoterAssociation ID
    "postId": string,          // 帖子 ID
    "postAlias": string,       // 帖子别名
    "postTitle": string | null,// 帖子标题
    "postCoverImage": string | null, // 帖子封面图
    "promoter": {
      "id": string,           // 推广者用户 ID
      "email": string | null, // 推广者邮箱
      "phoneNumber": string | null, // 推广者手机号
      "fullName": string,     // 推广者姓名
      "vanityUrl": string | null, // 推广者个性 URL
      "logo": string | null,  // 推广者头像
      "affiliateCode": string // 推广代码
    },
    "createdAt": string,       // 创建时间 (ISO 8601)
    "invitationType": string   // 邀请类型 (枚举值: "AFFILIATE" | "INVITATION_LINK")
  }
]

成功示例

HTTP 200 成功响应

[
  {
    "id": "123e4567-e89b-12d3-a456-426614174000",
    "postId": "1316f4a1-f5c7-465d-bb59-aaef9bbcbd47",
    "postAlias": "000011",
    "postTitle": "Summer Collection 2024",
    "postCoverImage": "https://cdn.example.com/cover.jpg",
    "promoter": {
      "id": "310ed6c9-dbf9-4b44-817d-54e8a940d4e8",
      "email": "neo.wang.ext+21@1m.app",
      "phoneNumber": null,
      "fullName": "Neo Wang",
      "vanityUrl": "neo-wang",
      "logo": "https://cdn.example.com/avatar.jpg",
      "affiliateCode": "ehf843"
    },
    "createdAt": "2026-02-26T13:45:00.000Z",
    "invitationType": "AFFILIATE"
  }
]

错误示例

404 Not Found - 帖子不存在

{
  "statusCode": 404,
  "message": "Resource not found",
  "error": "Not Found"
}

场景:指定的 postAlias 不存在或不属于当前用户。


注意事项 & 业务逻辑

数据来源

[!info] 数据来源 此接口查询的数据来自 PromoterAssociation 表,条件为:

  • curatorId = 当前用户 ID
  • invitationType = 'AFFILIATE'
  • deleted_at IS NULL

帖子 ID 从 note 字段的 JSON 元数据中解析:{"postId": "xxx", "createdAt": "..."}

与创建接口的配合

[!tip] 配合使用

  1. 调用 POST /curator/affiliate-link 创建联盟链接 → 自动创建 PromoterAssociation 记录
  2. 调用 GET /curator/affiliate-links 查询已创建的所有链接

业务规则

  1. 只返回自己创建的链接:只能查询自己作为 Curator 创建的联盟链接
  2. 帖子过滤:指定 postAlias 只返回该帖子的链接
  3. 去重逻辑:同一 Curator-Promoter 组合只保留一条记录(通过 deletedAt 软删除)
  4. 按创建时间倒序:最新创建的链接排在前面
  5. 无分页:一次性返回所有符合条件的记录

相关代码文件

文件路径 说明
src/promoter-association/promoter-association.controller.ts 控制器,新增 getPostAffiliateLinks 方法
src/promoter-association/promoter-association.service.ts 服务层,实现 getPostAffiliateLinkscreateAffiliateAssociationWithPost 方法
src/promoter-association/promoter-association.repo.ts 数据访问层,实现 findAffiliateAssociationsWithPromoterInfo 方法
src/promoter-association/promoter-association.interface.ts 接口定义,新增 GetPostAffiliateLinksQueryPostAffiliateLinkResponse

修改记录

日期 修改内容
2026-02-26 新增接口:查询帖子的联盟推广链接
2026-02-26 重要变更POST /curator/affiliate-link 现在会创建 PromoterAssociation 记录

results matching ""

    No results matching ""